Skip to content

feat(bench): Updated tx inclusion benchmarking#24266

Merged
PhilWindle merged 41 commits into
merge-train/spartan-v5from
phil/bench-integration-v5
Jul 2, 2026
Merged

feat(bench): Updated tx inclusion benchmarking#24266
PhilWindle merged 41 commits into
merge-train/spartan-v5from
phil/bench-integration-v5

Conversation

@PhilWindle

@PhilWindle PhilWindle commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

What

Lands the inclusion sweep on the v5 line — a nightly benchmark that deploys a live network and drives sustained 1 / 5 / 10 TPS mixed-priority load, measuring transaction inclusion under load. Supersedes the stacked PRs #24260 and #24265.

Changes

Benchmark harness & output

  • Run JSON schema v5 + scraper: proving-infra (hint-gen + proving-queue), per-role saturation (ELU / CPU / mem / event-loop delay), per-pod node resource sizing, RPC ingress duration, per-tx block-build decomposition, a pool-side pending→mined delay, and validator attestation-failure metrics (the signal upstream of quorum loss). run.benchmarkType lets the dashboard group a day's sweeps by kind.
  • inclusionTpsMean is a steady-state rate (warmup ramp + drain tail trimmed), so headlineKpi ≈ 1.0 on healthy runs.
  • reorgCount counts genuine chain reorgs only — events where the whole validator set pruned; a single/subset divergence is reported separately as nodeLocalPruneCount. Adds prune_count prune_type attribution for cause breakdown.
  • throughputTps (gossip / rpc / mined funnel) and p2pBandwidthBytesPerSec series.

Node-side metrics (aztec image)

  • Accurate pending→mined delay (now − receivedAt at the mined transition, no eviction conflation), widened mined-delay histogram buckets, and prune_count attributed by prune_type so pending-chain reorgs are countable from metrics instead of logs.

Deployment / CI

  • bench-inclusion-sweep.env (12×4 validators, 10 RPC, 10 full nodes, 72s slots, fake proving) + guaranteed-QoS resource profiles so every node gets reserved cores on an adequately-sized instance.
  • nightly-bench-inclusion-sweep.yml: deploy once, run 1/5/10 sequentially (own namespace each, shared sweepId), scrape-on-failure, GCS upload. Retires the single-point nightly-bench-10tps and the obsolete bench_10tps scenario.

Validation (smoke network, all fixes applied)

TPS Mined / failed Chain reorgs Checkpoint publish Client incl. P50 / P95
1 291 / 0 0 14/14 9.1s / 20.8s
5 1595 / 0 0 15/15 12.3s / 28.2s
10 3261 / 0 0 14/15 19.5s / 41.9s

Under-provisioned validators (bin-packed onto 2-core nodes) were the original reorg cause: attesters couldn't re-execute proposed blocks fast enough to hold quorum. The dedicated-core profiles fix it at the root — at 10 TPS, 13–15 reorgs → 0 and ~57% → ~93% checkpoint publish. The few single-node divergences seen under load self-healed with 0 tx loss and now count as nodeLocalPruneCount, not chain reorgs.

Follow-ups (not in this PR)

PhilWindle and others added 18 commits June 23, 2026 21:12
Phase 1 (A-1221) + Phase 2 (A-1222) of the nightly benchmarking revamp.

Schema v4 (additive over v3, all v3 fields retained):
- New optional sections provingInfra and saturation (metricSeriesMap: open
  slug -> timeSeries map), plus run.sweepId/sweepLabel so a night's 1/5/10 TPS
  points group as one sweep.
- Version gate bumped to "4" in bench_output.schema.json and the
  network_bench_upload check in bootstrap.sh; index.json entry now carries
  sweepId/sweepLabel. (Dashboard SUPPORTED_RUN_VERSION lives in
  AztecProtocol/explorations and must be bumped there separately.)
- 10tps-readiness-spec.md: the canonical spec — tx-lifecycle stage waterfall
  mapped to metrics, headline KPIs + thresholds, sweep/run-group notion.

Scraper (bench_scrape.ts):
- provingInfra: prover-node hint-gen (public_processor.* + prover_node
  block/checkpoint processing, scoped to the prover-node pod) and proving-queue
  series broken down by aztec_proving_job_type (size/active/job_duration/
  timed-out/resolved). There is no prover_node.execution.duration metric; the
  re-execution is the public_processor.* path, mapped accordingly.
- saturation: per-role ELU/CPU/memory as max (hottest pod) AND avg, never a
  single hand-picked pod. ELU/mem from nodejs.*, CPU from host-metrics.
- Sections scrape independently so one failure cannot drop the others.

CPU and ELU may be telemetry-gated in the bench env (empty series, non-fatal);
flagged in the spec for live verification per A-1222 acceptance.
…ch point

Phase 3 (A-1223), infra + runner. The CI workflow that fans out the 1/5/10
sweep is intentionally not included yet (concurrency model is an open decision).

- bench-inclusion-sweep.env: bench-10tps's custom scrape pipeline + the
  tps-scenario topology trimmed to 150 full nodes (12x4 validators, 10 RPC),
  local eth-devnet, fake (fixed-delay) proving so proving never bottlenecks
  inclusion, P2P instrumentation on. Target TPS / sweep id are passed per point.
- full-node-resources-spot.yaml: full-node profile that *requires* spot
  (nodeSelector pool:spot + required gke-spot affinity + toleration), copying
  the prover-resources-dev.yaml pattern. Validators/RPC/prover stay on-demand.
- spartan/bootstrap.sh bench_inclusion_point: one sweep point at TARGET_TPS in
  its own namespace, all high-value load, scraped with a shared BENCH_SWEEP_ID
  (schema v4) via the existing bench_scrape path; reuses network_bench_upload.

Usable for local runs now: TARGET_TPS=5 BENCH_SWEEP_ID=incl-<date> \
  spartan/bootstrap.sh bench_inclusion_point bench-inclusion-sweep
Phase 3 (A-1223) CI wiring for the Set A inclusion sweep.

- nightly-bench-inclusion-sweep.yml: runs the three points SEQUENTIALLY (one
  ~150-full-node network at a time to bound cluster load), each in its own
  namespace (bench-incl-sweep-{1,5,10}tps), all tagged with a shared sweepId
  computed once in select-image. Each point chains deploy -> wait -> bench ->
  teardown; the next point's deploy waits on the previous point's teardown, so
  peak load stays at one network. A failed point drops only itself.
- ci.sh network-inclusion-sweep: threads TARGET_TPS / BENCH_SWEEP_ID /
  BENCH_SWEEP_LABEL into the EC2 bootstrap command (mirrors network-bench-10tps).
- bootstrap.sh ci-network-inclusion-sweep: runs one point via
  spartan/bootstrap.sh bench_inclusion_point; v4 run JSON (tagged sweepId) is
  uploaded to GCS inside that function. SKIP_NETWORK_DEPLOY=1 (deploy/teardown
  done by the workflow), mirroring the ci-network-bench-10tps path.
Run #95's proving sweep stalled with the agent fleet parked around ~50
replicas: under fake-realistic delays (PUBLIC_VM ~180s/job) the ~50 agents
could not drain a 32-checkpoint epoch's jobs within the proof-submission
window, so the proven tip never advanced past epoch 1 and the run ended in a
proof-window-expiry reorg. The KEDA bands only requested the full 200 agents
once the unassigned queue exceeded 200; at moderate backlog the fleet was
capped at 10. Ramp to 75 agents at queue>25 and the full 200 at queue>100 so
a fresh epoch's job burst pulls in the fleet it needs.

Tuning hypothesis to validate on the next run; node-pool/spot autoscaling may
still gate how quickly 200 agents actually schedule.
… series

Verified against a live smoke run: the v4 scraper's memory and duration-histogram
series came back empty because the OTel->Prometheus exporter appends unit
suffixes the queries omitted:
- nodejs heap gauge is nodejs_memory_v8_heap_usage_bytes (was ..._usage)
- duration histograms carry a _milliseconds infix, e.g.
  aztec_proving_queue_job_duration_milliseconds_bucket,
  aztec_public_processor_tx_duration_milliseconds_bucket,
  aztec_prover_node_{block,checkpoint}_processing_duration_milliseconds_bucket

With these, memory (all roles), hint-gen, and proving-queue job_duration series
now populate. ELU + CPU were already correct. Full-node saturation remains
empty by design (FULL_NODE_INCLUDE_METRICS restricts full nodes to gossip).
…test fails

bench_inclusion_point ran the scrape only if the load test succeeded — a
non-zero test exit aborted the function first, discarding the inclusion records
and per-role/proving metrics it had already captured. A degraded point (e.g. a
higher-TPS point that misses its target, or any failed assertion) is exactly
where that data is most useful. Capture the test exit code, always scrape +
upload, then re-surface the failure via the return code so job status still
reflects it.
Two robustness changes to n_tps.test.ts, surfaced by a smoke run where the
inclusion benchmark mined 300/300 txs but still exited code 1:

- Replace the strict "successCount === totalHighValueSent" assertion with
  metrics.recordInclusionOutcome(mined, failed). A degraded point (a TPS target
  the network can't fully include) should report its mined-vs-failed counts +
  success_ratio, not fail the run — the ratio is itself the headline result we
  track. A total submission failure (nothing sent) is still a hard error.
- Wrap the afterAll wallet-cleanup loop and uninstallChaosMesh in try/catch so a
  teardown error (dead PXE, missing chaos-mesh CRDs on a run that didn't install
  it) is logged, not thrown — it was turning successful runs red.

tx_metrics.ts: TxInclusionMetrics gains recordInclusionOutcome, emitting
inclusion/mined_count, inclusion/failed_count, inclusion/success_ratio.
…value + low-value fill)

Each inclusion-sweep point now drives a fixed 1 TPS of high-value txs (the
measured inclusion lane) plus (TARGET_TPS - 1) TPS of low-value background
traffic, instead of all-high-value at the target. This matches bench_10tps and
measures what we actually care about: whether a properly-paying user's tx is
included promptly while the network runs at the target total TPS. The 10 TPS
point is now identical to bench-10tps (1 high + 9 low).
…eorgs

The high-value success/fail count came from waitForTx(waitForStatus: PROPOSED),
which throws/times out if a reorg drops a mined tx back to pending or the pool
evicts it — so a tx that WAS included got counted as failed under reorg churn
(inflating failed_count on reorg-heavy high-TPS runs). Inclusion is a
first-sighting event: the block-watcher already stamps minedAtMs idempotently
(never cleared on reorg). Add TxInclusionMetrics.wasMined(txHash) and, in the
waitForTx catch path, count the tx as included if it was ever observed in a
block — we don't care what happens to it afterwards.
Add ingressTxDurationP50/P99 from aztec.node.receive_tx.duration (histogram,
RPC-side tx-ingest cost on the submission path). This is the metric to watch
when RPC ingress is the bottleneck — it climbs as the RPC saturates, distinct
from the ingress-TPS rate. Flows into the existing timeSeries section (open map,
no schema change).
Reuse MEMPOOL_TX_MINED_DELAY but derive the value from the tx's persisted
receivedAt at the mined transition in handleMinedBlock, instead of the
add/remove timestamp map. The map-based recording fired on any removal
(eviction/deletion too), conflating mined delay with evicted delay; it was
also in-memory and lost across restarts.

handleMinedBlock now passes a per-tx minedDelayMs (now - receivedAt) through
onTxsMined, left undefined for txs hydrated from the DB on restart
(receivedAt === 0) so unknown receive times don't pollute the histogram. The
shared PoolInstrumentation map mechanism is retained for the attestation pool.
The pending-to-mined delay (aztec.mempool.tx_mined_delay) routinely exceeds the
60s ceiling of the shared ms-unit histogram view under load, saturating every
percentile at 60000. Add a name-scoped view with buckets from 1s to 10min,
placed before the generic ms view so it wins the bucket boundaries (the SDK
keeps the first-registered compatible storage; other ms histograms are
unaffected). Also fix the now-stale metric description.
…e event-loop delay

Three additive metrics in the v4 scrape output:
- Per-tx block build decomposition in summary: blockBuildPerTxMsAggregate,
  blockBuildMarginalMsPerTx (OLS slope), blockBuildFixedOverheadMsPerBlock
  (OLS intercept), blockBuildAvgTxsPerBlock — computed from per-block records.
- Pool-side pending-to-mined delay (aztec_mempool_tx_mined_delay) as both
  timeSeries (mempoolTxMinedDelayP50/P95/P99) and summary scalars, distinct
  from the client-observed inclusionLatency*.
- Event-loop delay (nodejs_eventloop_delay_mean) added to per-role saturation,
  surfacing the prover node's multi-second main-thread blockage at 10 TPS.
Validator and RPC pods previously requested only 0.5 CPU with a node-type=network
selector, so the scheduler bin-packed them across the shared cluster's mixed
2-/4-core network pools. Validators landing on 2-core nodes ran their 4 attesters
on too little CPU, fell behind on re-execution, and failed to attest ('Timed out
waiting for block with archive matching checkpoint proposal') — dragging the
committee below quorum and causing checkpoint prunes (reorgs) at 10 TPS.

Add bench resource profiles (request=limit, Guaranteed QoS) so every node gets
dedicated, un-contended cores and lands on an adequately-sized node:
- validator: 3.5 CPU / 12Gi  (one pod, 4 attesters, per dedicated 4-core node)
- rpc:       3.5 CPU / 8Gi   (dedicated 4-core; drops prod's preferred cores=2 affinity)
- full-node: 3.5 CPU / 8Gi   (kept on spot; packs onto 8-/32-core spot nodes)

Point both bench-inclusion-sweep envs at VALIDATOR/RPC/FULL_NODE_RESOURCE_PROFILE=bench.
Prover/broker keep dev-hi-tps (already explicitly sized, never bin-packed).
Adds a dedicated 4-core spot pool (spot_nodes_4core, t2d-standard-4) to the GKE
cluster. Benchmark full nodes request 3.5 CPU but there was no 4-core spot pool
(spot pools were 2/8/32-core), so they packed several onto 8-core spot boxes.
Pin the bench full-node profile to the new pool via a cores=4 nodeSelector so
each full node gets a dedicated 4-core spot node, without consuming the
on-demand pool that validators / RPC / prover run on.

Requires a terraform apply of the gke-cluster/cluster module before the next
bench deploy, otherwise the cores=4 + pool=spot full-node pods stay Pending.
…2s slots

The inclusion sweep (1/5/10 TPS) is a superset of the single-point 10 TPS
nightly (it reuses the bench-10tps load model), so retire nightly-bench-10tps.yml
and let nightly-bench-inclusion-sweep.yml be the nightly TPS/inclusion benchmark.

Rebase bench-inclusion-sweep.env on the smoke env (same 12x4 validator / 10 RPC /
10 full-node topology on dedicated guaranteed-QoS bench profiles, RPC ingress
off, fake proving) but at a representative 72s slot duration and 800 tx per
checkpoint. 8-slot epochs keep nightly committee warm-up short. Test duration is
already 10 min (bench_inclusion_point default TEST_DURATION_SECONDS=600; the
workflow does not override it).
Replace bench-inclusion-sweep.env with the smoke env's topology (12x4 validators,
10 RPC, 10 full nodes on dedicated guaranteed-QoS bench profiles, RPC ingress off,
fake proving, 8-slot epochs) but at a representative 72s slot duration and 800 tx
per checkpoint. Drops the prior production timing / RPC LoadBalancer / per-block
cap that the sweep no longer uses.
@PhilWindle PhilWindle changed the title [DO NOT MERGE] bench integration — all Set A / schema / reliability changes for testing feat(bench): Set A inclusion sweep (1/5/10 TPS nightly) + deterministic node sizing Jun 26, 2026
The inclusion-sweep workflow repeated the deploy -> wait -> bench -> teardown
block once per TPS point (~250 lines of near-duplicate jobs). Extract it into a
reusable workflow (bench-inclusion-point.yml, workflow_call) parametrized by
tps/namespace/image/ref/sweep_id, and call it 3x from the sweep with needs
chaining for the sequential one-network-at-a-time ordering. An `if: !cancelled`
guard on the later points preserves point independence (a failed point drops
only itself). Sweep workflow drops 357 -> 161 lines; per-point logic defined once.
…l/bench-integration-v5

# Conflicts:
#	.github/workflows/nightly-bench-inclusion-sweep.yml
…etwork

Previously each TPS point deployed and tore down its own network (3x deploy +
teardown). Switch to deploying a single network, running the 1/5/10 points
sequentially against it (the scraper drains the mempool to zero between points),
and tearing it down once. Cheaper, faster, and matches how it's run locally.

deploy/wait/teardown move to the top-level workflow (run once); the reusable
bench-inclusion-point.yml is now just the bench step (one point, against the
existing network, SKIP_NETWORK_DEPLOY=1), called 3x with needs chaining so the
points don't overlap. Later points are gated on `wait` success so a failed
point still drops only itself.
Capture each pod's aztec-container CPU/memory requests and limits during
the infrastructure scrape, exposed as `resources` per node and a deduped
`resourceProfiles` per role, so a run records the machine sizing the
validators/rpc/full-node/prover pods actually got (request==limit for the
guaranteed-QoS bench profiles).

Also anchor the role-classification patterns to the namespace-stripped pod
name so the L1 infra pods (eth-validator / eth-execution / eth-beacon) are
no longer misread as aztec roles — an unanchored /-validator/ matched
"<ns>-eth-validator-0" and polluted the validator role.
The node emits aztec.validator.attestation_failed_node_issue_count (the
attester-side timeout/missing-data counter that diagnosed the 10 TPS reorgs)
alongside the bad-proposal and success counters, but the bench scraper never
queried them — only the proposer-side collect-duration gauges reached the JSON.

Add three timeSeries (node-issue and bad-proposal failures broken down by
error_type, plus a success rate for a failure-ratio denominator) and three
summary scalars totalling each outcome over the observed window, so the
attestation node-issue count — the signal upstream of committee quorum loss —
is recorded per run.
prune_count was incremented only by the proven/epoch prune (handleEpochPrune),
so the pending-chain reorgs that drive world-state's "Chain pruned to block N"
— proposed blocks dropped because their slot ended without a checkpoint, because
they conflicted with an L1 checkpoint, or because their matching proposed
checkpoint never arrived before the deadline (pruneOrphanBlocks) — incremented
no metric and were observable only in logs.

Add a prune_type attribute (unproven / uncheckpointed / l1_conflict / orphan) to
prune_count and tag all four emit sites. Scraped per pod, this distinguishes a
single straggler node's local re-sync from a network-wide reorg.
…breakdown

reorgCount counted `Chain pruned` log events network-wide, which conflates one
straggler node repeatedly re-syncing its local view with a genuine network
reorg. The 10 TPS sweep showed this: 3 "reorgs" that were all one saturated
validator pruning its own view three times while the canonical chain never
moved.

Re-point reorgCount to count DISTINCT PODS that experienced a pending-chain
prune (archiver prune_count, prune_type in uncheckpointed/l1_conflict/orphan),
so a straggler reads as 1 and a network reorg as many. Add a prunesByType
summary breakdown and a pruneCountByType time series. deepestReorgBlocks stays
log-derived (no depth metric). Requires the prune_type-attributed metric (new
image); reads 0 on older images, cross-checked by the chainPruned events.
Add a throughputTps series carrying three labeled sub-series (source =
gossip | rpc_receive | mined) so the dashboard overlays tx rate at three stages
on one chart: gossip propagation (avg per-node accepted tx-topic gossipsub
messages — every node accepts each unique tx once, so avg approximates the
unique-tx propagation rate; sum would overcount by pod count), RPC ingress
(receiveTx on the submission node), and mined (archiver block tx rate, avg
across archivers). Query validated against retained Prometheus data.
Add p2pBandwidthBytesPerSec carrying four labeled series (recv/sent × avg/max
per pod) from the gossipsub RPC byte counters. Per pod rather than summed so the
hottest-pod (max) series surfaces gossip bandwidth that isn't evenly spread — at
10 TPS the hottest pod moved ~4 MB/s each way vs ~2.3 MB/s average. No
libp2p-level transport metric is exported, so the gossipsub byte counters are
the bandwidth signal. Query validated against retained Prometheus data.
…licate-tf-blocks

The spot_nodes_4core node pool definition belongs with the gke-cluster terraform
cleanup, not the bench branch. Removed here and applied to
stack/chore-remove-duplicate-tf-blocks. The pool is already applied to the live
aztec-gke-private cluster, and the bench full-node resource profile that targets
it (cores=4 selector) stays on this branch.
NETWORK_BENCH_BUCKET overrides the upload target (default unchanged:
gs://aztec-testnet/network_bench) so the upload path can be validated against a
test prefix without polluting the production dashboard index. Add a
network_bench_upload dispatch entry so a run JSON can be (re)uploaded manually.
Strip internal tracker issue IDs and the 'Set A' project label from code and
workflow comments; the repo is public so these convey nothing to external
readers. Behaviour unchanged (comment-only edits).
@PhilWindle PhilWindle changed the title feat(bench): Set A inclusion sweep (1/5/10 TPS nightly) + deterministic node sizing feat(bench): inclusion sweep (1/5/10 TPS nightly) + deterministic node sizing Jun 29, 2026
The headline inclusion TPS divided total mined by the whole observed window,
which includes the warmup ramp and the post-load drain tail — diluting it well
below the achieved rate (e.g. 0.84 at a 1 TPS target that actually kept up).

Compute it instead from the block log over [first block with txs, load stop]:
trims the startup ramp (start at first real inclusion) and the cooldown tail
(end at load stop, not drain end). headlineKpi now reads ~1.0 on healthy runs
(1/5/10 TPS: 0.97/0.99/0.99) and drops below 1 only on genuine shortfall. The
old whole-window figure is retained as inclusionTpsWindowMean.
Tag each chainPruned event with podCount (distinct pods that pruned to the same
block within the dedupe window). reorgCount now counts only genuine chain reorgs
(podCount >= 2 — the canonical chain rewound across nodes); single-node local
prunes (a straggler that diverged and re-synced) are counted separately as
nodeLocalPruneCount and no longer inflate reorgCount or deepestReorgBlocks. This
is log-derived, so it is accurate regardless of node image. Verified on the
1/5/10 TPS runs: 0 chain reorgs, with the 5/10 TPS prunes correctly classified
as node-local.
Refine reorg detection: tag each chainPruned event with validatorPruneCount
(distinct validator pods that pruned to that block) and compare against the
validator fleet size (distinct validators that logged l2-block-handled).
reorgCount now counts only events where ALL following validators rewound to the
same block — a true canonical reorg. A prune reported by a subset of validators
(or by non-validator followers) is a local divergence (nodeLocalPruneCount), not
a reorg. Verified on 1/5/10 TPS: validatorCount=12, 0 chain reorgs (prunes had
0-1 validators each).
Adds an optional run.benchmarkType enum (ingress-inclusion, simulated-proving,
real-proving, block-capacity) so the dashboard can group a day's sweeps by
benchmark kind. Bumps the run schema 4->5 and the index schema 1->2.

- bench_scrape.ts: --benchmark-type arg / BENCH_BENCHMARK_TYPE env, emitted in run
- bootstrap.sh: inclusion point passes ingress-inclusion; benchmarkType copied
  into each index.json entry; upload gate now expects schema "5"
RPC_RESOURCE_PROFILE="bench"
RPC_INGRESS_ENABLED=false

FULL_NODE_REPLICAS=10

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept low until we know it all works


AZTEC_EPOCH_DURATION=8
AZTEC_SLOT_DURATION=72
AZTEC_PROOF_SUBMISSION_EPOCHS=100

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effectively never re-org

TestDateProvider.now() tracks the real clock, so minedDelayMs was the 5s
advance plus the real elapsed time between addPendingTxs and handleMinedBlock —
which could exceed the < 6000ms bound on a slow/loaded CI box. Switch to
ManualDateProvider (frozen clock, advances only via advanceTime) so the delay
is deterministically exactly 5000ms.
…tstrap

The single-shot 10 TPS bench (bench_10tps / bench_10tps_cmds) is superseded by
the inclusion sweep (bench_inclusion_point, run at 1/5/10 TPS). Removes the dead
functions and the case-dispatch entry; the shared scraper at
scripts/bench_10tps/bench_scrape.ts is still used by the sweep and is untouched.
@PhilWindle
PhilWindle marked this pull request as ready for review June 30, 2026 11:09
@PhilWindle
PhilWindle requested a review from charlielye as a code owner June 30, 2026 11:09
@PhilWindle PhilWindle changed the title feat(bench): inclusion sweep (1/5/10 TPS nightly) + deterministic node sizing feat(bench): Updated tx inclusion benchmarking Jun 30, 2026

@spalladino spalladino left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Some scripts I skimmed through, and I flagged one possible incompatibility on schema version that Claude spotted, plus a prune category missing.

//
// Scrape a completed bench-10tps run into a schema-conformant JSON payload.
// Contract: bench_output.schema.json (v3). Invoked by the bench_10tps function
// Contract: bench_output.schema.json (v4). Invoked by the bench_10tps function

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up bench_output.schema.json shows v5, not v4.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, thanks for this.

Comment on lines 2246 to 2249
if (payload.schemaVersion !== "4") {
throw new Error(
`schemaVersion must be "3", got ${String(payload.schemaVersion)}`,
`schemaVersion must be "4", got ${String(payload.schemaVersion)}`,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude found this incompatibility: schema version if 5 (as created a few lines below), but here we request 4.

@spalladino spalladino Jul 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing a recordPrune in L785 (next to a call to removeCheckpointsAfter), which triggers when the latest checkpointed tip in the archiver does not match the one on L1, which could be caused by either an L1 reorg or a missed proof.

And I'm happy that I found it, and Claude missed it. How sad is that?

Comment on lines +683 to +691
// Once a tx has been observed in a block we count it as included, even if
// waitForTx then threw because a reorg dropped it back to pending or the
// pool evicted it. Inclusion is a first-sighting event here; we don't care
// what happens to the tx afterwards.
if (metrics.wasMined(txHash)) {
logger.info(`${txName} was mined (ignoring post-inclusion reorg/drop)`, { txName, txHash });
results.push({ success: true, txHash });
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC the only thing that can set the wasMined for a tx is recordMinedTx, which is never reached if waitForTx throws.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 2 places that update the underlying store for mined tx data. recordMinedTx and an observer that watches blocks for mined transactions.

…atch prune

- bench_scrape.ts asserted schemaVersion "4" while emitting "5", so the scraper
  threw on its own output; align the check and refresh the stale header comment
  (bench_10tps -> bench_inclusion_point).
- Archiver: the checkpointed-tip-vs-L1 mismatch rewind (removeCheckpointsAfter in
  l1_synchronizer) emitted no prune_count. Add a new 'l1_mismatch' prune_type for it
  (distinct from the proposed-block causes) and surface it in prunesByType + schema.
Comment on lines +87 to +89
this.pruneCount = createUpDownCounterWithDefault(meter, Metrics.ARCHIVER_PRUNE_COUNT, {
[Attributes.PRUNE_TYPE]: ['unproven', 'uncheckpointed', 'l1_conflict', 'orphan'],
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to update our alerts so they sum over all prune types, but this is 🔥

l1_mismatch was added to recordPrune and emitted from the l1 synchronizer, but omitted from the prune_count default-values list, so the prune_count{prune_type="l1_mismatch"} series only appeared after the first such prune instead of from startup like the other causes. Add it to the zero-seed list and to the PRUNE_TYPE attribute doc comment.
@PhilWindle
PhilWindle enabled auto-merge (squash) July 2, 2026 19:28
@PhilWindle
PhilWindle merged commit c7673d1 into merge-train/spartan-v5 Jul 2, 2026
12 checks passed
@PhilWindle
PhilWindle deleted the phil/bench-integration-v5 branch July 2, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants